﻿/*
    Copyright (c) 2012, iD Commerce + Logistics
    All rights reserved.

    Redistribution and use in source and binary forms, with or without modification, are permitted
    provided that the following conditions are met:

    Redistributions of source code must retain the above copyright notice, this list of conditions
    and the following disclaimer. Redistributions in binary form must reproduce the above copyright
    notice, this list of conditions and the following disclaimer in the documentation and/or other
    materials provided with the distribution.
 
    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
    IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
    FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
    CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
    CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
    SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
    THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
    OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
    POSSIBILITY OF SUCH DAMAGE.
*/

using System;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using System.Data.Entity;
using System.Threading;
using System.Data.Entity.Infrastructure;
using System.Collections.Generic;
using System.Diagnostics;

namespace IdComLog.Extensions
{
    /// <summary>
    /// Extension methods for IQueryable.
    /// </summary>
    public static class QueryableExtensions
    {
        /// <summary>
        /// Selects a into a single object which matches a predicate.
        /// </summary>
        /// <typeparam name="T">The type of item to search.</typeparam>
        /// <typeparam name="TRet">The return type of the select function.</typeparam>
        /// <param name="src">The items to search.</param>
        /// <param name="where">A predicate which matches a single item.</param>
        /// <param name="select">A function which selects from the item.</param>
        /// <returns>An object selected from the item.</returns>
        public static TRet SelectSingleWhere<T, TRet>(this IQueryable<T> src, Expression<Func<T, bool>> where, Expression<Func<T, TRet>> select)
        {
            if (src == null) throw new ArgumentNullException("src");
            if (where == null) throw new ArgumentNullException("where");
            if (select == null) throw new ArgumentNullException("select");

            return src.Where(where).Select(select).Single();
        }

        /// <summary>
        /// Asynchronously enumerates the query results and performs the specified asynchronous action on each element.
        /// </summary>
        /// <remarks>
        /// Multiple active operations on the same context instance are not supported.  Use 'await' to ensure
        /// that any asynchronous operations have completed before calling another method on this context.
        /// </remarks>
        /// <param name="source">
        /// An <see cref="T:System.Linq.IQueryable" /> to enumerate.
        /// </param>
        /// <param name="action">The asynchronous action to perform on each element.</param>
        /// <returns>A task that represents the asynchronous operation.</returns>
        public static Task ForEachAsync(this IQueryable source, Func<object, Task> action)
        {
            return ForEachAsync(source, action, CancellationToken.None);
        }

        /// <summary>
        /// Asynchronously enumerates the query results and performs the specified asynchronous action on each element.
        /// </summary>
        /// <remarks>
        /// Multiple active operations on the same context instance are not supported.  Use 'await' to ensure
        /// that any asynchronous operations have completed before calling another method on this context.
        /// </remarks>
        /// <param name="source">
        /// An <see cref="T:System.Linq.IQueryable" /> to enumerate.
        /// </param>
        /// <param name="action">The asynchronous action to perform on each element.</param>
        /// <param name="cancellationToken">
        /// A <see cref="T:System.Threading.CancellationToken" /> to observe while waiting for the task to complete.
        /// </param>
        /// <returns>A task that represents the asynchronous operation.</returns>
        public static async Task ForEachAsync(this IQueryable source, Func<object, Task> action, CancellationToken cancellationToken)
        {
            if (source == null) throw new ArgumentNullException("source");
            if (action == null) throw new ArgumentNullException("action");

            IDbAsyncEnumerable e = source as IDbAsyncEnumerable;

            if (e == null)
            {
                throw new InvalidOperationException("The source IQueryable doesn't implement IDbAsyncEnumerable. Only sources that implement IDbAsyncEnumerable can be used for Entity Framework asynchronous operations. For more details see http://go.microsoft.com/fwlink/?LinkId=287068.");
            }

            using (IDbAsyncEnumerator ee = e.GetAsyncEnumerator())
            {
                Task<bool> task = ee.MoveNextAsync(cancellationToken);

                while (await task.ConfigureAwait(false))
                {
                    object current = ee.Current;
                    task = ee.MoveNextAsync(cancellationToken);
                    await action(current).ConfigureAwait(false);
                }
            }
        }

        /// <summary>
        /// Asynchronously enumerates the query results and performs the specified asynchronous action on each element.
        /// </summary>
        /// <remarks>
        /// Multiple active operations on the same context instance are not supported.  Use 'await' to ensure
        /// that any asynchronous operations have completed before calling another method on this context.
        /// </remarks>
        /// <typeparam name="T">
        /// The type of the elements of <paramref name="source" />.
        /// </typeparam>
        /// <param name="source">
        /// An <see cref="T:System.Linq.IQueryable`1" /> to enumerate.
        /// </param>
        /// <param name="action">The asynchronous action to perform on each element.</param>
        /// <param name="cancellationToken">
        /// A <see cref="T:System.Threading.CancellationToken" /> to observe while waiting for the task to complete.
        /// </param>
        /// <returns>A task that represents the asynchronous operation.</returns>
        public static async Task ForEachAsync<T>(this IQueryable<T> source, Func<T, Task> action, CancellationToken cancellationToken)
        {
            if (source == null) throw new ArgumentNullException("source");
            if (action == null) throw new ArgumentNullException("action");

            IDbAsyncEnumerable<T> e = source as IDbAsyncEnumerable<T>;

            if (e == null)
            {
                throw new InvalidOperationException("The source IQueryable doesn't implement IDbAsyncEnumerable<" + typeof(T) + ">. Only sources that implement IDbAsyncEnumerable can be used for Entity Framework asynchronous operations. For more details see http://go.microsoft.com/fwlink/?LinkId=287068.");
            }

            using (IDbAsyncEnumerator<T> ee = e.GetAsyncEnumerator())
            {
                Task<bool> task = ee.MoveNextAsync(cancellationToken);

                while (await task.ConfigureAwait(false))
                {
                    T current = ee.Current;
                    task = ee.MoveNextAsync(cancellationToken);
                    await action(current).ConfigureAwait(false);
                }
            }
        }

        /// <summary>
        /// Exposes a queryable sequence as an explicitly async sequence.
        /// </summary>
        /// <typeparam name="T">The type returned by the query.</typeparam>
        /// <param name="source">The queryable sequence.</param>
        /// <returns>An asynchronous enumerator.</returns>
        public static IAsyncEnumerable<T> AsAsyncEnumerable<T>(this IQueryable<T> source)
        {
            if (source == null) throw new ArgumentNullException("source");

            IDbAsyncEnumerable<T> e = source as IDbAsyncEnumerable<T>;

            if (e == null)
            {
                throw new InvalidOperationException("The source IQueryable doesn't implement IDbAsyncEnumerable<" + typeof(T) + ">. Only sources that implement IDbAsyncEnumerable can be used for Entity Framework asynchronous operations. For more details see http://go.microsoft.com/fwlink/?LinkId=287068.");
            }

            return new DbAsyncEnumerableAdaptor<T>(e);
        }

        sealed class DbAsyncEnumerableAdaptor<T> : IAsyncEnumerable<T>
        {
            readonly IDbAsyncEnumerable<T> e;

            public DbAsyncEnumerableAdaptor(IDbAsyncEnumerable<T> e)
            {
                Debug.Assert(e != null);
                this.e = e;
            }

            public IAsyncEnumerator<T> GetEnumerator()
            {
                return new DbAsyncEnumeratorAdaptor<T>(e.GetAsyncEnumerator());
            }
        }

        sealed class DbAsyncEnumeratorAdaptor<T> : IAsyncEnumerator<T>
        {
            IDbAsyncEnumerator<T> e;

            public DbAsyncEnumeratorAdaptor(IDbAsyncEnumerator<T> e)
            {
                Debug.Assert(e != null);
                this.e = e;
            }

            public T Current { get { return e.Current; } }

            public Task<bool> MoveNext(CancellationToken cancellationToken)
            {
                return e.MoveNextAsync(cancellationToken);
            }

            public void Dispose()
            {
                if (e != null)
                {
                    e.Dispose();
                    e = null;
                }
            }
        }

    }
}
